package orpio

import (
	"appengine"
	"appengine/datastore"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"html/template"
	"io"
	"log"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// Datastore unit.
type Url struct {
	LongUrl  string
	ShortUrl string
	Created  time.Time
}

// POST request-response for /new.
type newRequest struct {
	LongUrl string
}

type newResponse struct {
	LongUrl  string
	ShortUrl string
}

// getShortUrl generates and returns a short url of length kShortUrlLen for
// the given url.
func getShortUrl(url string) (string, error) {
	if url == "" {
		return "", fmt.Errorf("Empty request")
	}
	h := sha1.New()
	io.WriteString(h, salt)
	io.WriteString(h, url)
	sum := h.Sum(nil)
	b := make([]byte, base64.URLEncoding.EncodedLen(len(sum)))
	base64.URLEncoding.Encode(b, sum)
	return string(b)[:kShortUrlLen], nil
}

// validateUrl returns an error if the given string is not a valid URL.
func validateUrl(s string) error {
	u, err := url.Parse(s)
	if err != nil {
		return err
	}
	u.Scheme = strings.ToLower(u.Scheme)
	switch {
	case u.Scheme != "http" && u.Scheme != "https":
		return fmt.Errorf("URL scheme allowed only to be http or https.")
	case u.Host == "":
		return fmt.Errorf("Missing Host in URL")
	case !strings.Contains(u.Host, "."):
		return fmt.Errorf("Missing . in URL")
	}
	return nil
}

// validateShortUrl returns an error if the given string is an invalid shortUrl.
func validateShortUrl(shortUrl string) error {
	if len(shortUrl) != kShortUrlLen {
		return fmt.Errorf("Invalid length: Should be %d, is %d.", kShortUrlLen, len(shortUrl))
	}
	return nil
}

// Initialization.
var templates *template.Template
var router = mux.NewRouter()
var kShortUrlLen = 7
var salt = ""

func init() {
	var err error
	if templates, err = template.New("").ParseFiles("templates/base.html"); err != nil {
		log.Fatal(err)
	}
	router.HandleFunc("/", rootHandler)
	router.HandleFunc("/alive", aliveHandler)
	router.HandleFunc("/new", newHandler)
	router.HandleFunc(
		fmt.Sprintf("/{shortUrl:[-_a-zA-Z0-9]{%d}}", kShortUrlLen),
		redirectHandler)

	http.Handle("/", router)
}

// The main / handler.
func rootHandler(w http.ResponseWriter, r *http.Request) {
	templates.ExecuteTemplate(w, "base.html", nil)
}

// alive handler for tests.
func aliveHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "ok")
}

// POST /new handler.
func newHandler(w http.ResponseWriter, r *http.Request) {
	// Forbid all but POST requests.
	if r.Method != "POST" {
		http.Error(w, "Only POST requests are allowed.", http.StatusForbidden)
		return
	}

	c := appengine.NewContext(r)

	// Get JSON request from client.
	var req newRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err == io.EOF {
		http.Error(w, "Missing request", http.StatusBadRequest)
		return
	} else if err != nil {
		http.Error(w,
			fmt.Sprintf("JSON decode error: %v", err),
			http.StatusBadRequest)
		return
	}

	// Validate Long URL.
	longUrl := req.LongUrl
	if err := validateUrl(longUrl); err != nil {
		http.Error(w,
			fmt.Sprintf("Invalid URL \"%s\" [%v]", longUrl, err),
			http.StatusBadRequest)
		return
	}

	// Generate Short URL.
	shortUrl, err := getShortUrl(longUrl)
	if err != nil {
		http.Error(w,
			fmt.Sprintf("Error generating id: %v", err),
			http.StatusBadRequest)
		return
	}

	// Save mapping.
	newUrl := &Url{ShortUrl: shortUrl, LongUrl: longUrl, Created: time.Now()}
	key := datastore.NewKey(c, "Url", shortUrl, 0, nil)
	_, errd := datastore.Put(c, key, newUrl)
	if errd != nil {
		http.Error(w,
			fmt.Sprintf("Error writing to datastore: %v", errd),
			http.StatusInternalServerError)
		return
	}

	// Create response.
	resp := newResponse{
		LongUrl:  longUrl,
		ShortUrl: shortUrl,
	}
	b, err := json.Marshal(resp)
	if err != nil {
		http.Error(w,
			fmt.Sprintf("Error marshalling data: %v [%v]", resp, err),
			http.StatusInternalServerError)
		return
	}

	// Send response back to client.
	_, errw := w.Write(b)
	if errw != nil {
		c.Errorf("Error writing to response: %v", errw)
	}
}

// The redirect handler.
func redirectHandler(w http.ResponseWriter, r *http.Request) {
	// Get requested short URL and validate.
	v := mux.Vars(r)
	shortUrl := v["shortUrl"]
	if err := validateShortUrl(shortUrl); err != nil {
		http.Error(w, fmt.Sprintf("Invalid short URL: %s", shortUrl),
			http.StatusBadRequest)
		return
	}

	// Retrieve from datastore.
	c := appengine.NewContext(r)
	key := datastore.NewKey(c, "Url", shortUrl, 0, nil)
	var newUrl Url
	err := datastore.Get(c, key, &newUrl)
	if err != nil {
		http.Error(w, fmt.Sprintf("datastore error: %v", err),
			http.StatusInternalServerError)
		return
	}

	// Redirect to long URL.
	http.Redirect(w, r, newUrl.LongUrl, http.StatusFound)
}
